/* Mandelbrot.c is a small program that produces a Mandelbrot set, which can
   then be zoomed in on, and saved as a Bitmap or copied to the clipboard.
   The program by default supports multithreading, to help speed up the
   intensive computation. The user can choose the resolution and the number
   of threads to use. 
   Copyright (C) 2012  James Boyle

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.*/

#include <windows.h>
#include <process.h>
#include <tchar.h>
#include "Bitmap.h"
#include "Render.h"
#include "Mandelbrot.h"
#include "resource.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
                   PSTR pstrCmdLine, int iCmdShow)
{
	static TCHAR szAppName[] = TEXT("Mandelbrot");
	HWND hWindow;
	MSG message;
	WNDCLASS windowClass;
	HACCEL hAccel;

	windowClass.style = CS_HREDRAW | CS_VREDRAW; 
	windowClass.lpfnWndProc = WindowProcedure; 
	windowClass.cbClsExtra = 0;
	windowClass.cbWndExtra = 0;
	windowClass.hInstance = hInstance;
	windowClass.hIcon = LoadIcon(hInstance, szAppName);
	windowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
	windowClass.hbrBackground = NULL;
	windowClass.lpszMenuName = szAppName;
	windowClass.lpszClassName = szAppName;

	if (!RegisterClass(&windowClass))
	{
		MessageBox(NULL, TEXT("This program requires Windows NT"),
		           szAppName, MB_ICONERROR);
		return 0;
	}
	
	hWindow = CreateWindow(szAppName, TEXT("Mandelbrot"),
	                       WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
	                       CW_USEDEFAULT, CW_USEDEFAULT, 
	                       CW_USEDEFAULT, NULL, NULL, hInstance, NULL); 
	hAccel = LoadAccelerators(hInstance, szAppName);
	ShowWindow(hWindow, iCmdShow);
	UpdateWindow(hWindow);

	/* message loop */
	while (GetMessage(&message, NULL, 0, 0))
	{
		if (!TranslateAccelerator(hWindow, hAccel, &message))
		{
			TranslateMessage(&message);
			DispatchMessage(&message);
		}
	}
	
	return message.wParam; 
}

LRESULT CALLBACK WindowProcedure(HWND hWindow, UINT message,
                                 WPARAM wParam, LPARAM lParam)
{ 
	BOOL bSuccess;
	static RENDERDETAILS currRender;
	static THREADPARAM *pThreadParams;
	static DIALOGPARAM dialogParams;
	static MANDELBROTDATA progData;

	switch (message)
	{
	case WM_CREATE:
		progData.hInstance = ((LPCREATESTRUCT) lParam)->hInstance;
		
		bSuccess = InitialiseMandelbrot(&progData, &currRender, 
		                                &dialogParams, hWindow);
		/* if an error was encountered in initialisation, terminate the program */
		if (!bSuccess)
			return -1;
		pThreadParams = InitialiseThreads(&progData, hWindow);

		break;

	case WM_LBUTTONDOWN:
		LeftButtonDown(&progData, &currRender, lParam, hWindow);
		break;

	case WM_LBUTTONUP:
		LeftButtonUp(&progData, &currRender);
		break;

	/* the right mouse button cancels any zooming in selection */
	case WM_RBUTTONUP:
		RightButtonUp(&progData, hWindow);
		break;

	case WM_MOUSEMOVE:
		MouseMove(&progData, &currRender, wParam, lParam, hWindow);
		break;

	/* the RENDER_DONE message indicates a thread has finished processing */
	case WM_RENDER_DONE:
		/* for this message wParam holds whether the thread was successful
		   (TRUE value) or cancelled (FALSE value) */
		bSuccess = wParam;
		
		/* if the thread finished rendering, copy the rendered section (of the 
		   thread that sent the message) into the render bitmap, by bltting */
		currRender.iFinishedThreads++;
		if (bSuccess)
			ProcessThreadOutput(&progData, &currRender, pThreadParams, 
			                    (int) lParam, hWindow); 

		/* if all threads have now finished execution, stop the timer,
		   mark the bRendering flag as false and blt to the screen buffer */
		if (currRender.iFinishedThreads == currRender.iThreads)
			FinishRender(&progData, &currRender, pThreadParams, hWindow);
		
		break;

	case WM_SIZE:
		progData.xClientWidth = LOWORD(lParam);
		progData.yClientHeight = HIWORD(lParam);
		break;

	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		case IDM_RENDER_START:
			StartRender(&progData, &currRender, pThreadParams, &dialogParams, 
			            hWindow);
			break;

		case IDM_RENDER_STOP:
			if (currRender.bRendering)
				CancelRender(&currRender, pThreadParams, hWindow);
			break;

		case IDM_RENDER_PROPERTIES:
			DialogBoxParam(progData.hInstance, TEXT("Properties"), hWindow, 
			               PropertiesProcedure, (LPARAM) &dialogParams);
			break;

		case IDM_RENDER_RESET:
			ResetProperties(&progData, &dialogParams);
			break;

		case IDM_EDIT_COPY:
			Copy(&progData, hWindow);
			break;

		case IDM_FILE_SAVE: 
			SaveFile(&progData, &currRender, hWindow);
			break;

		case IDM_FILE_EXIT:
			SendMessage(hWindow, WM_CLOSE, 0, 0);
			break;
		}
		
		break;

	case WM_PAINT: 
		Paint(&progData, &currRender, hWindow);
		break;

	case WM_DESTROY:
		FinishMandelbrot(&progData, &currRender, pThreadParams, hWindow);
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWindow, message, wParam, lParam);
	}
	return 0;
}

/* InitialiseMandelbrot() must be called before any of the Mandelbrot functions
   are used.  The function returns TRUE if successful. */
BOOL InitialiseMandelbrot(MANDELBROTDATA *const pData, 
                          RENDERDETAILS *const pCurrRender, 
                          DIALOGPARAM *const pDialogParams,
                          HWND hWindow)
{
	HDC hdc;
	SYSTEM_INFO sysInfo;
	static TCHAR szFilter[] = 
		TEXT("Bitmap Files (*.BMP)\0*.bmp\0All Files (*.*)\0*.*\0\0");

	hdc = GetDC(hWindow);

	/* get the number of CPU cores, and create either twice as many threads
		as cores, or 4 threads depending on which number is larger. */
	GetSystemInfo(&sysInfo);
	pCurrRender->iCores = sysInfo.dwNumberOfProcessors;
	pData->iMaxThreads = 
		((pCurrRender->iCores * 2) > 4) ? (pCurrRender->iCores * 2) : 4;

	/* the default number of active threads is twice the number of cores */
	pDialogParams->iCurrThreadOption = IDC_RADIO_DEFAULT;

	/* allocate memory so each thread can draw to a section of the screen */
	pCurrRender->hRenderRegions = NULL; 
	pCurrRender->hRenderRegions = 
		(HBITMAP *) malloc(pData->iMaxThreads * sizeof(HBITMAP));
	if (pCurrRender->hRenderRegions == NULL)
		return FALSE;
		
	/* get the current resolution */
	pCurrRender->xScreenRes = GetDeviceCaps(hdc, HORZRES);
	pCurrRender->yScreenRes = GetDeviceCaps(hdc, VERTRES);
	pDialogParams->iCurrResolutionOption = DEFAULT_RESOLUTION;
		
	/* initialise the OPENFILESTRUCT, which is used in the "save" dialog */ 
	InitialiseOfnStruct(&pData->ofn, szFilter, pData->szFileName, 
	                    pData->szTitleName, hWindow);
		
	/* allocate the bitmap buffers and initialise them to a default colour */ 
	pData->hScreenBuffer = CreateCompatibleBitmap(hdc, pCurrRender->xScreenRes, 
		                                          pCurrRender->yScreenRes);
	pData->hRender = CreateCompatibleBitmap(hdc, pCurrRender->xScreenRes, 
	                                        pCurrRender->yScreenRes);
	if (pData->hScreenBuffer == NULL || pData->hRender == NULL)
		return FALSE;
	
	InitialiseBitmap(GetSysColor(COLOR_APPWORKSPACE), pData->hScreenBuffer, 
	                 pCurrRender->xScreenRes, pCurrRender->yScreenRes);
	DefaultRange(pData);
	
	pCurrRender->bRendering = FALSE;
	pData->bDisplayRender = FALSE;
	pData->bZoom = FALSE;
	InvalidateRect(hWindow, NULL, FALSE);
	ReleaseDC(hWindow, hdc);

	return TRUE;
}

/* LeftButtonUp() may cause the rendering range to be recalculated and the cursor 
   reset to default */
void LeftButtonUp(MANDELBROTDATA *const pData, 
                  RENDERDETAILS *const pCurrRender)
{
	if (pData->bZoom)
	{
		SetCursor(LoadCursor(NULL, IDC_ARROW));
		CalculateRange(pCurrRender, &pData->currRange, 
		               &pData->zoomSelection);
		ReleaseCapture();
	}
}

/* LeftButtonDown() may start the selection of a region of the Mandelbrot set */
void LeftButtonDown(MANDELBROTDATA *const pData,
                    RENDERDETAILS *const pCurrRender, 
                    const LPARAM lParam, HWND hWindow)
{
	long xCursorPos, yCursorPos, xImageWidth, yImageHeight;
	
	if (pData->bDisplayRender)
	{
		xCursorPos = LOWORD(lParam);
		yCursorPos = HIWORD(lParam);
		xImageWidth = pCurrRender->xImageRes;
		yImageHeight = pCurrRender->yImageRes;

		pData->selectionStart.x = 
			(xCursorPos < xImageWidth) ? xCursorPos : xImageWidth;
		pData->selectionStart.y = 
			(yCursorPos < yImageHeight) ? yCursorPos : yImageHeight;

		pData->bZoom = TRUE;
		pData->zoomSelection.bottom = pData->selectionStart.y;
		pData->zoomSelection.top = pData->selectionStart.y;
		pData->zoomSelection.left = pData->selectionStart.x;
		pData->zoomSelection.right = pData->selectionStart.x;

		SetCursor(LoadCursor(NULL, IDC_CROSS)); 
		SetCapture(hWindow);

		InvalidateRect(hWindow, NULL, FALSE);
	}
}

/* RightButtonUp() may result in the current selection being canceled */
void RightButtonUp(MANDELBROTDATA *const pData, HWND hWindow)
{
	if (pData->bZoom)
	{
		pData->bZoom = FALSE;
		InvalidateRect(hWindow, NULL, FALSE);
	}
}

/* MouseMove() deals with the WM_MOUSEMOVE message, which if the left mouse
   button is pressed down, may extend a dotted rectangle on the screen */
void MouseMove(MANDELBROTDATA *const pData, RENDERDETAILS *const pCurrRender, 
               WPARAM wParam, LPARAM lParam, HWND hWindow)
{
	/* if the user is able to zoom in and the left button is down */
	if (pData->bZoom && (wParam & MK_LBUTTON))
	{
		UpdateZoomSelection(lParam, pData->selectionStart, &pData->zoomSelection, 
		                    pCurrRender->xImageRes, pCurrRender->yImageRes);
		InvalidateRect(hWindow, NULL, FALSE);
	}
}

/* SaveFile() saves the last rendered image to file */
void SaveFile(MANDELBROTDATA *const pData, RENDERDETAILS *const pCurrRender, 
              HWND hWindow)
{
	HDC hdc, hdcBuffer;
	BOOL bSuccess;

	/* save output as device independent bitmap to file */
	bSuccess = GetSaveFileName(&pData->ofn);
	/* if user cancelled save, by cancelling the dialog, stop save */
	if (!bSuccess)
		return;
			
	/* otherwise save the file */
	hdc = GetDC(hWindow);
	hdcBuffer = CreateCompatibleDC(hdc);
	bSuccess = SaveBitmap(pData->szFileName, pData->hRender, 
	                      pCurrRender->xImageRes, 
	                      pCurrRender->yImageRes);
			
	/* produce output based on success of save */
	if (bSuccess)
		MessageBox(hWindow, TEXT("File saved successfully."), 
				    TEXT("Save"), MB_OK | MB_ICONINFORMATION);
	else
		MessageBox(hWindow, TEXT("Error: Couldn't save file."),
				    TEXT("Save"), MB_OK | MB_ICONERROR);
			
	ReleaseDC(hWindow, hdc);
	DeleteDC(hdcBuffer);
}

/* Paint() deals with the workload of the WM_PAINT message */
void Paint(MANDELBROTDATA *const pData, RENDERDETAILS *const pCurrRender,
           HWND hWindow)
{
	HDC hdc, hdcBuffer, hdcRender;
	PAINTSTRUCT ps;

	/* drawing is done through double buffering to avoid flicker,
	   hScreenBuffer holds the next screen update */
	hdc = BeginPaint(hWindow, &ps);
	hdcRender = CreateCompatibleDC(hdc);
	hdcBuffer = CreateCompatibleDC(hdc);
	SelectObject(hdcRender, pData->hRender);
	SelectObject(hdcBuffer, pData->hScreenBuffer);
		
	if (pData->bDisplayRender)
	{
		BitBlt(hdcBuffer, 0, 0, pData->xClientWidth, pData->yClientHeight, 
		       hdcRender, 0, 0, SRCCOPY);
		TextOut(hdcBuffer, 0, 0, pData->szTime, _tcslen(pData->szTime));
		if (pData->bZoom)
			DrawFocusRect(hdcBuffer, &pData->zoomSelection);
	}

	BitBlt(hdc, 0, 0, pData->xClientWidth, pData->yClientHeight, hdcBuffer, 
	       0, 0, SRCCOPY);

	DeleteDC(hdcRender);
	DeleteDC(hdcBuffer);
	EndPaint(hWindow, &ps);
}

/* ResetProperties() sets all the dialog box properties and ranges back to the
   default ie the values they were at the start of the program */
void ResetProperties(MANDELBROTDATA *const pData, 
                     DIALOGPARAM *const pDialogParams)
{
	pDialogParams->iCurrResolutionOption = DEFAULT_RESOLUTION;
	pDialogParams->iCurrThreadOption = IDC_RADIO_DEFAULT;
			
	pData->currRange.imagMax = 1.0;
	pData->currRange.imagMin = -1.0;
	pData->currRange.realMax = 1.0;
	pData->currRange.realMin = -2.0;
}

/* Copy() copies the last rendered image to the clipboard */
void Copy(MANDELBROTDATA *const pData, HWND hWindow)
{
	HDC hdc;
	HANDLE hClipboardBitmap;

	/* copy contents to the clipboard */
	hdc = GetDC(hWindow);
			
	hClipboardBitmap = CopyBitmap(pData->hRender);
	OpenClipboard(hWindow);
	EmptyClipboard();
	SetClipboardData(CF_BITMAP, hClipboardBitmap);
			
	CloseClipboard();
	ReleaseDC(hWindow, hdc);
}

/* FinishMadelbrot() must be called before the program finishes, cleaning up
   any resources used by the program */
void FinishMandelbrot(MANDELBROTDATA *const pData, 
                      RENDERDETAILS *const pRenderDetails, 
                      THREADPARAM *const pThreadParams, HWND hWindow)
{
	/* if the program is currently rendering, cancel it */
	if (pRenderDetails->bRendering)
		CancelRender(pRenderDetails, pThreadParams, hWindow);
		
	/* kill all the rendering threads if they've been created */
	if (pThreadParams)
	{
		KillThreads(pData->iMaxThreads, pThreadParams);
		/* wait until all threads have been terminated, so the resources are
		   now freed up */
		WaitForMultipleObjects(pData->iMaxThreads, pData->hThreadKillEvent, 
		                       TRUE, INFINITE);
	}

	/* deallocate any space held by the threads */
	free(pThreadParams);
	free(pRenderDetails->hRenderRegions);
	free(pData->hThreadKillEvent);

	/* deallocate the screen buffers */
	DeleteObject(pData->hScreenBuffer);
	DeleteObject(pData->hRender);
}

BOOL CALLBACK PropertiesProcedure(HWND hDialog, UINT message, WPARAM wParam, 
                                  LPARAM lParam)
{
	int i;
	static DIALOGPARAM *pParam;
	static int iResolutionOption, iThreadOption;
	TCHAR szComboText[RESOLUTIONS][15] = {TEXT("Default"), TEXT("480x360"), 
		TEXT("640x480"), TEXT("800x600"), TEXT("1280x1024")};

	switch (message)
	{
	case WM_INITDIALOG:
		pParam = (DIALOGPARAM *) lParam;
		/* initialise the combo box */
		for (i = 0; i < RESOLUTIONS; i++)
		{
			SendDlgItemMessage(hDialog, IDC_COMBO1, CB_ADDSTRING, 0, 
			                   (LPARAM) szComboText[i]);
		}

		iThreadOption = pParam->iCurrThreadOption;
		iResolutionOption = pParam->iCurrResolutionOption;
		CheckRadioButton(hDialog, IDC_RADIO_1, IDC_RADIO_DEFAULT, iThreadOption);
		SendDlgItemMessage(hDialog, IDC_COMBO1, CB_SETCURSEL, 
		                   iResolutionOption, 0);
		return TRUE;

	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		case IDC_RADIO_DEFAULT:
		case IDC_RADIO_1:
		case IDC_RADIO_2:
		case IDC_RADIO_4:
			iThreadOption = LOWORD(wParam);
			return TRUE;
		case IDOK:
			/* permanently store the variables in the DIALOGPARAM struct */
			pParam->iCurrThreadOption = iThreadOption;
			pParam->iCurrResolutionOption = 
				SendDlgItemMessage(hDialog, IDC_COMBO1, CB_GETCURSEL, 0, 0);
			/* fall through */
		case IDCANCEL:
			EndDialog(hDialog, 0);
			return TRUE;
		}
	default:
		return FALSE;
	}
}

/* GetThreadCount() simply takes the "Properties" dialog box variables and
   works out the chosen number of threads to use */
void GetThreadCount(RENDERDETAILS *const pRenderDetails, 
                    DIALOGPARAM *const pDialogParams)
{
	UINT iThreads;

	switch (pDialogParams->iCurrThreadOption)
	{
	case IDC_RADIO_1:
		iThreads = 1;
		break;
	case IDC_RADIO_2:
		iThreads = 2;
		break;
	case IDC_RADIO_4:
		iThreads = 4;
		break;
	default:
		iThreads = pRenderDetails->iCores * 2;
		break;
	}

	pRenderDetails->iThreads = iThreads;
}

/* GetResolution() takes the "Properties" dialog box variables and works out
   the chosen resolution */
void GetResolution(RENDERDETAILS *const pRenderDetails, 
                   DIALOGPARAM *const pDialogParams)
{
	int iOption = pDialogParams->iCurrResolutionOption;
	const int xResolutions[] = {480, 640, 800, 1280};
	const int yResolutions[] = {360, 480, 600, 1024};

	if (iOption == DEFAULT_RESOLUTION)
	{
		pRenderDetails->xImageRes = pRenderDetails->xScreenRes;
		pRenderDetails->yImageRes = pRenderDetails->yScreenRes;
	}
	else
	{
		pRenderDetails->xImageRes = xResolutions[iOption-1];
		pRenderDetails->yImageRes = yResolutions[iOption-1];
	}
}

/* UpdateZoomSelection() takes a starting point, and the current mouse position
   and updates the selection rectangle */
void UpdateZoomSelection(const LPARAM lParam, const POINT start, 
                         RECT *const rect, const int xImageWidth, 
                         const int yImageHeight)
{
	int xCursorPos, yCursorPos;
	xCursorPos = LOWORD(lParam);
	yCursorPos = HIWORD(lParam);
	/* if x position of mouse is to the right of starting position */
	if (xCursorPos > start.x)
	{
		rect->left = start.x;
		/* make sure the cursor is within the rendered image */
		rect->right = (xCursorPos < xImageWidth) ? xCursorPos : xImageWidth;
	}
	else /* if x position is to the left */
	{
		rect->left = xCursorPos;
		rect->right = start.x;
	}
	/* if y position of mouse is lower than starting position */
	if (yCursorPos > start.y)
	{
		rect->top = start.y;
		/* make sure the cursor is within the rendered image */
		rect->bottom = (yCursorPos < yImageHeight) ? yCursorPos : yImageHeight;
	}
	else /* if y position is higher */
	{
		rect->top = HIWORD(lParam);
		rect->bottom = start.y;
	}
}

/* CalculateRange() works out the new interval to draw the set over, by
   calculating the maximum and minimum imaginary and real values defined
   by the selected region */
void CalculateRange(RENDERDETAILS *const pRenderDetails, 
                    RENDERRANGE *const pRange, RECT *const zoomSelection)
{
	RENDERRANGE newRange;
	double oldImagMax, oldImagMin, oldRealMax, oldRealMin;
	long yRes, xRes, yMaxPos, yMinPos;

	oldImagMax = pRenderDetails->range.imagMax;
	oldImagMin = pRenderDetails->range.imagMin;
	oldRealMax = pRenderDetails->range.realMax;
	oldRealMin = pRenderDetails->range.realMin;
	yRes = pRenderDetails->yImageRes;
	xRes = pRenderDetails->xImageRes;

	/* flip the imaginary value to compensate for the the windows 
	   coordinate system */
	yMaxPos = yRes - zoomSelection->top;
	yMinPos = yRes - zoomSelection->bottom;
	
	/* calculate maximum and minimum values for the real and imaginary
	   components */
	newRange.imagMax = 
		NormaliseVal(oldImagMin, oldImagMax, yRes, yMaxPos);
	newRange.imagMin =
		NormaliseVal(oldImagMin, oldImagMax, yRes, yMinPos);
	newRange.realMax =
		NormaliseVal(oldRealMin, oldRealMax, xRes, zoomSelection->right);
	newRange.realMin =
		NormaliseVal(oldRealMin, oldRealMax, xRes, zoomSelection->left);
	
	/* these values are now the new range for the set */
	*pRange = newRange;
}

/* DefaultRange() initialises the rendering region to some default values */
void DefaultRange(MANDELBROTDATA *const pData)
{
	pData->currRange.imagMax = 1.0;
	pData->currRange.imagMin = -1.0;
	pData->currRange.realMax = 1.0;
	pData->currRange.realMin = -2.0;
}

/* StartRender() is called whenever the user wants to start another render
   job, and is responsible for creating the bitmap objects and device contexts 
   that the threads will use, and working out what portion of the screen each 
   thread has.  It also gets an initial time, updates the menu, and once 
   everything is set, signals the threads' event objects. */
void StartRender(MANDELBROTDATA *const pData, 
                 RENDERDETAILS *const pRenderDetails, 
                 THREADPARAM *const pThreadParams, 
                 DIALOGPARAM *const pDialogParams, 
                 HWND hWindow)
{
	UINT i;
	int xRes, yRes, xWidth;
	HDC hdc;
	HMENU hMenu;

	hdc = GetDC(hWindow);
	hMenu = GetMenu(hWindow);
	
	/* copy across details e.g. the resolution, the number of threads 
	   to use from the dialog properties structure */
	pData->bDisplayRender = FALSE;
	pData->bZoom = FALSE;
	GetThreadCount(pRenderDetails, pDialogParams);
	GetResolution(pRenderDetails, pDialogParams);
	pRenderDetails->range = pData->currRange;

	/* set the cursor to IDC_WAIT to show program is rendering */
	SetCursor(LoadCursor(NULL, IDC_WAIT));
	SetClassLong(hWindow, GCL_HCURSOR, (LONG) LoadCursor(NULL, IDC_WAIT));
	
	/* get an initial reading from a high resolution timer to measure the 
		running time of the threads */
	pRenderDetails->dElapsedTime = 0.0;
	QueryPerformanceCounter(&pRenderDetails->initialTime);
	pRenderDetails->iFinishedThreads = 0;
	pRenderDetails->bRendering = TRUE;
	
	/* initialise the screen buffer */
	InitialiseBitmap(GetSysColor(COLOR_APPWORKSPACE), pData->hScreenBuffer, 
	                 pRenderDetails->xScreenRes, pRenderDetails->yScreenRes);
	/* initialise the bitmap which stores the rendered image */
	InitialiseBitmap(GetSysColor(COLOR_APPWORKSPACE), pData->hRender, 
	                 pRenderDetails->xScreenRes, pRenderDetails->yScreenRes);

	/* update the menu */
	EnableMenuItem(hMenu, IDM_RENDER_START, MF_GRAYED);
	EnableMenuItem(hMenu, IDM_RENDER_STOP, MF_ENABLED);
	EnableMenuItem(hMenu, IDM_FILE_SAVE, MF_GRAYED);
	EnableMenuItem(hMenu, IDM_EDIT_COPY, MF_GRAYED);

	/* initialise each thread's parameters */
	for (i = 0; i < pRenderDetails->iThreads; i++)
	{
		xRes = pRenderDetails->xImageRes;
		yRes = pRenderDetails->yImageRes;

		/* cut the screen into segments to be individually rendered */
		pThreadParams[i].plotArea.left = 
			(xRes / pRenderDetails->iThreads) * i;
		pThreadParams[i].plotArea.right = 
			(xRes / pRenderDetails->iThreads) * (i + 1);
		pThreadParams[i].plotArea.top = 0;
		pThreadParams[i].plotArea.bottom = yRes;
		
		/* create the bitmap for the thread to write to */
		pThreadParams[i].hdcRender = CreateCompatibleDC(hdc);
		xWidth = xRes / pRenderDetails->iThreads;
		pRenderDetails->hRenderRegions[i] = 
			CreateCompatibleBitmap(hdc, xWidth, yRes);
		SelectObject(pThreadParams[i].hdcRender, 
		             pRenderDetails->hRenderRegions[i]);
		
		pThreadParams[i].hWindow = hWindow;
		pThreadParams[i].xImageRes = xRes;
		pThreadParams[i].yImageRes = yRes;
		pThreadParams[i].range = pData->currRange;
		
		/* set the event object, basically telling the thread to start 
		   rendering */
		SetEvent(pThreadParams[i].hSignal);
	}

	InvalidateRect(hWindow, NULL, FALSE);
	ReleaseDC(hWindow, hdc);
}

/* CancelRender() is called when the user wants to stop a render, and simply
   sets every thread's status to CANCELLED. It's the thread's job to send the 
   WM_RENDER_DONE message after stopping, which then allows FinishRender() to 
   clean up. */
void CancelRender(RENDERDETAILS *const pRenderDetails, 
     THREADPARAM *const pThreadParams, HWND hWindow)
{
	UINT i;
	
	/* don't terminate the threads, just set their status to CANCELLED, which
	   indicates to them to stop any rendering */
	for (i = 0; i < pRenderDetails->iThreads; i++)
	{
		/* if thread is currently rendering, cancel it */
		if (pThreadParams[i].threadStatus == RENDERING)
			pThreadParams[i].threadStatus = CANCELLED;
	}
}

/* FinishRender() is called after every thread has finished, either after the 
   render is completed or cancelled.  It calculates the elapsed time, resets 
   the cursor, updates the menu and deallocates the bitmap objects and device 
   contexts. */
void FinishRender(MANDELBROTDATA *const pData, 
                  RENDERDETAILS *const pRenderDetails,
                  THREADPARAM *const pThreadParams, 
                  HWND hWindow)
{
	UINT i;
	HMENU hMenu;

	hMenu = GetMenu(hWindow);
	/* get a final reading from a high resolution timer and the timer
		frequency, allowing you to calculate the time in seconds that
		the rendering took */
	QueryPerformanceCounter(&pRenderDetails->endTime);
	QueryPerformanceFrequency(&pRenderDetails->frequency);

	/* update the menu to allow saving and copying */
	EnableMenuItem(hMenu, IDM_RENDER_START, MF_ENABLED);
	EnableMenuItem(hMenu, IDM_RENDER_STOP, MF_GRAYED);
	EnableMenuItem(hMenu, IDM_FILE_SAVE, MF_ENABLED);
	EnableMenuItem(hMenu, IDM_EDIT_COPY, MF_ENABLED);

	/* set the cursor back to the default */
	SetCursor(LoadCursor(NULL, IDC_ARROW));
	SetClassLong(hWindow, GCL_HCURSOR, 
	             (LONG) LoadCursor(NULL, IDC_ARROW));
	
	/* work out the time taken to render the image in seconds */
	pRenderDetails->dElapsedTime = (double)
		pRenderDetails->endTime.QuadPart - pRenderDetails->initialTime.QuadPart;
	pRenderDetails->dElapsedTime /= pRenderDetails->frequency.QuadPart;

	/* free all device contexts and bitmap objects */
	if (pThreadParams)
	{
		for (i = 0; i < pRenderDetails->iThreads; i++)
			DeleteDC(pThreadParams[i].hdcRender);
	}

	if (pRenderDetails->hRenderRegions)
	{
		for (i = 0; i < pRenderDetails->iThreads; i++)
			DeleteObject(pRenderDetails->hRenderRegions[i]);
	}

	/* mark the render as finished */
	pRenderDetails->bRendering = FALSE;

	pData->bDisplayRender = TRUE;
	/* output the elapsed time */
	_stprintf_s(pData->szTime, TIME_STRING_LEN, TEXT("%f seconds elapsed"), 
	            pRenderDetails->dElapsedTime);
	InvalidateRect(hWindow, NULL, FALSE);
}

/* InitialiseOfnStruct() initialises a large struct that is used by the 
   GetFileSaveName() dialog box.  The struct is then used when saving 
   the render as a Device Independent Bitmap. */
void InitialiseOfnStruct(OPENFILENAME *ofn, TCHAR *szFilter, TCHAR *szFileName,
                         TCHAR *szTitleName, HWND hWindow)
{
	ofn->lStructSize = sizeof(OPENFILENAME);
	ofn->hwndOwner = hWindow;
	ofn->hInstance = NULL;
	ofn->lpstrFilter = szFilter;
	ofn->lpstrCustomFilter = NULL;
	ofn->nMaxCustFilter = 0;
	ofn->nFilterIndex = 0;
	ofn->lpstrFile = szFileName;
	ofn->nMaxFile = MAX_PATH;
	ofn->lpstrFileTitle = szTitleName;
	ofn->lpstrInitialDir = NULL;
	ofn->lpstrTitle = NULL;
	ofn->Flags = OFN_OVERWRITEPROMPT;
	ofn->nFileOffset = 0;
	ofn->nFileExtension = 0;
	ofn->lpstrDefExt = TEXT("bmp");
	ofn->lCustData = 0;
	ofn->lpfnHook = NULL;
	ofn->lpTemplateName = NULL;
}

/* ThreadProcedure() is the procedure for the rendering threads, and simply waits
   for the main thread to set an event object, before making a call to
   MandelbrotRender().  The thread procedure can be terminated and restarted. */
void ThreadProcedure(PVOID pVoid)
{
	THREADPARAM *pParams;
	BOOL bSuccess;

	pParams = (THREADPARAM *) pVoid;
	while (1)
	{
		/* wait for the event object to be signalled, basically where the
		   main thread wants to trigger the thread into action */
		WaitForSingleObject(pParams->hSignal, INFINITE);
		if (pParams->bKill)
			break;
		MandelbrotRender(pParams);
		
		/* once the thread has finished processing, send a WM_RENDER_DONE
		   message to the main thread.  wParam is a BOOL that says whether 
		   the render was successful (TRUE) or cancelled (FALSE) and lParam
		   is the thread's ID number */
		if (pParams->threadStatus == CANCELLED)
			bSuccess = FALSE;
		else 
			bSuccess = TRUE;
		PostMessage(pParams->hWindow, WM_RENDER_DONE, bSuccess,
		            pParams->threadID);
		/* reset the thread's status */
		pParams->threadStatus = IDLE;
	}
	SetEvent(pParams->hThreadKilled);
	_endthread();
}

/* InitialiseThreads() allocates memory for the threads and initialises the 
   essential parameters - bKill, hSignal and threadID - which are required 
   for the entire life of the thread, and then creates the threads with 
   _beginthread().  The threads run until the program is ended.  This function 
   creates the maximum possible number of threads, then if a smaller number is 
   required, some will stay idle. */
THREADPARAM *InitialiseThreads(MANDELBROTDATA *const pData, HWND hWindow)
{
	int i;
	TCHAR szError[] = TEXT("Error: Can't pass 0 as number of threads to ")
					  TEXT("InitialiseThreads().");
	THREADPARAM *pThreadParams = NULL;

	/* perform some elementary sanity check on the number of threads */
	if (pData->iMaxThreads == 0)
	{
		MessageBox(hWindow, szError, TEXT("Error"), MB_OK | MB_ICONERROR);
		return NULL;
	}

	pThreadParams = NULL;
	pData->hThreadKillEvent = NULL;
	pData->hThreadKillEvent = 
		(HANDLE *) malloc(pData->iMaxThreads * sizeof(HANDLE));
	
	if (pData->hThreadKillEvent == NULL)
		return NULL;
	
	pThreadParams = 
		(THREADPARAM *) malloc(pData->iMaxThreads * sizeof(THREADPARAM));
	
	if (pThreadParams == NULL)
		return NULL;

	for (i = 0; i < pData->iMaxThreads; i++)
	{
		pThreadParams[i].threadID = i;
		/* set the event object so it automatically resets i.e. the thread
		   only renders once, and so it's initially unset i.e. the thread
		   must be manually started */
		pThreadParams[i].hSignal = CreateEvent(NULL, FALSE, FALSE, NULL);
		pData->hThreadKillEvent[i] = CreateEvent(NULL, FALSE, FALSE, NULL); 
		pThreadParams[i].hThreadKilled = pData->hThreadKillEvent[i];
			
		pThreadParams[i].bKill = FALSE;
		/* start the thread */
		_beginthread(ThreadProcedure, 0, &pThreadParams[i]);
	}
	return pThreadParams;
}

/* ProcessThreadOuput() is called after a thread finishes its job, and blts the
   output to a single bitmap */
void ProcessThreadOutput(MANDELBROTDATA *const pData, 
                         RENDERDETAILS *const pRenderDetails, 
                         THREADPARAM *const pThreadParams, 
                         const int threadID, HWND hWindow)
{
	HDC hdc, hdcRender;
	int xWidth, yHeight;
	THREADPARAM *pParams;

	/* create the buffer device context */
	hdc = GetDC(hWindow);
	hdcRender = CreateCompatibleDC(hdc);
	SelectObject(hdcRender, pData->hRender);

	pParams = &pThreadParams[threadID];
	xWidth = pParams->plotArea.right - pParams->plotArea.left;
	yHeight = pParams->plotArea.bottom - pParams->plotArea.top;
	BitBlt(hdcRender, pParams->plotArea.left, pParams->plotArea.top,
	       xWidth, yHeight, pParams->hdcRender, 0, 0, SRCCOPY);
	
	DeleteDC(hdcRender);
	ReleaseDC(hWindow, hdc);
}

/* KillThreads() iterates through every thread and kills it by setting the 
   bKill flag to TRUE */
void KillThreads(const UINT iThreads, THREADPARAM *const pThreadParams)
{
	UINT i;

	for (i = 0; i < iThreads; i++)
	{
		pThreadParams[i].bKill = TRUE;
		SetEvent(pThreadParams[i].hSignal);
	}
}